Skip to content

Update dispatchEvent documentation for clarity#43521

Open
yuval-a wants to merge 4 commits intomdn:mainfrom
yuval-a:patch-3
Open

Update dispatchEvent documentation for clarity#43521
yuval-a wants to merge 4 commits intomdn:mainfrom
yuval-a:patch-3

Conversation

@yuval-a
Copy link
Copy Markdown

@yuval-a yuval-a commented Mar 22, 2026

Description

Clarified the difference between native events and manually dispatched events in the dispatchEvent documentation.

Motivation

The previous phrasing implied that the actual event handler functions themselves are scheduled asynchronously on the Event Loop, but only the dispatch is.

Additional details

Related issues and pull requests

https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent

Fixes #43519

Clarified the difference between native events and manually dispatched events in the dispatchEvent documentation.

The previous phrasing implied that the actual event handler functions themselves are scheduled asynchronously on the Event Loop, but only the dispatch is.
@yuval-a yuval-a requested a review from a team as a code owner March 22, 2026 22:30
@yuval-a yuval-a requested review from dipikabh and removed request for a team March 22, 2026 22:30
@github-actions github-actions Bot added Content:WebAPI Web API docs size/xs [PR only] 0-5 LoC changed labels Mar 22, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 23, 2026

Preview URLs (1 page)

(comment last updated: 2026-03-27 14:07:25)

@dipikabh
Copy link
Copy Markdown
Contributor

Thanks for the PR, @yuval-a!

Could you add to your PR description the line "Fixes #43519"?
That way they'll be linked and the issue will close when this PR is merged.

Comment thread files/en-us/web/api/eventtarget/dispatchevent/index.md Outdated
This rephrase clarifies that what is async for native events is the dispatch process itself, _not_ the actual event handlers.

Better phrasing after @dipikabh remark.
@github-actions github-actions Bot added size/s [PR only] 6-50 LoC changed and removed size/xs [PR only] 0-5 LoC changed labels Mar 26, 2026
Copy link
Copy Markdown
Author

@yuval-a yuval-a left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dipikabh how's this?

asynchronously via the [event loop](/en-US/docs/Web/JavaScript/Reference/Execution_model),
`dispatchEvent()` invokes event handlers _synchronously_. All applicable event
handlers are called and return before `dispatchEvent()` returns.
Unlike calling `dispatchEvent()` manually, "native" events are fired by the browser and dispatched asynchronously via the [event loop](/en-US/docs/Web/JavaScript/Reference/Execution_model). The dispatch process itself is similar in both cases and invokes event handlers _synchronously_. All applicable event handlers are called and return before `dispatchEvent()` returns.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rewrite ends up comparing two different things - "calling" and "events".
I suggest we either keep the original phrasing ("Unlike "native" events...")
OR
break it down into two sentences and also change the voice to active to differentiate between browser and developer actions. ("when you call" is a subtle hint that you call this method manually)

What do you think about:

Suggested change
Unlike calling `dispatchEvent()` manually, "native" events are fired by the browser and dispatched asynchronously via the [event loop](/en-US/docs/Web/JavaScript/Reference/Execution_model). The dispatch process itself is similar in both cases and invokes event handlers _synchronously_. All applicable event handlers are called and return before `dispatchEvent()` returns.
The browser queues "native" events on the [event loop](/en-US/docs/Web/JavaScript/Reference/Execution_model) and fires them asynchronously. In contrast, when you call `dispatchEvent()`, it invokes event handlers synchronously. Note that `dispatchEvent()` returns only after all applicable event handlers have executed.

@Josh-Cena could you cross-check the technical accuracy of my suggestion here. Thanks!

Copy link
Copy Markdown
Member

@Josh-Cena Josh-Cena Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks right to me :) I would suggest:

Suggested change
Unlike calling `dispatchEvent()` manually, "native" events are fired by the browser and dispatched asynchronously via the [event loop](/en-US/docs/Web/JavaScript/Reference/Execution_model). The dispatch process itself is similar in both cases and invokes event handlers _synchronously_. All applicable event handlers are called and return before `dispatchEvent()` returns.
The browser queues "native" events on the [event loop](/en-US/docs/Web/JavaScript/Reference/Execution_model#job_queue_and_event_loop), so each event handler is executed asynchronously in a separate job. In contrast, when you call `dispatchEvent()`, it invokes event handlers synchronously and returns only after all applicable event handlers have executed.

Copy link
Copy Markdown
Author

@yuval-a yuval-a Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Josh-Cena - but that phrasing brings back the technical confusion/inaccuracy that this PR aims to correct from the first place.

Correct me if I'm wrong.

The difference to "native" - is that the call for the dispatch itself is async, but during it, event listeners run synchronously - don't go up the event loop.

The statement

"each event handler is executed asynchronously[...]"

makes it sound as if each of them is scheduled asynchronously on the event loop.

Interestingly, I tried to physically verify this by running some code in the console:

const btn = document.createElement('button'); 
btn.textContent = 'click me'; 
document.body.appendChild(btn); 
btn.addEventListener('click', () => { 
   console.log('listener 1 start'); 
   Promise.resolve().then(() => { 
      console.log('microtask from listener 1'); 
   }); 
   setTimeout(() => { console.log('timeout from listener 1'); }, 0); 
   console.log('listener 1 end'); 
}); 
btn.addEventListener('click', () => { console.log('listener 2'); }); 

The microtask log does run between listeners, the timeout log only after both run.
When running the same principle with a manual dispatchEvent and a custom event - the microtask log only shows after both listeners run. In native - there is a microtask checkpoint between listeners. But, still - the listeners themselves doesn't run async, no separate async queuing for each...
Or are they...? I mean, this test shows at least that no async timers run between (tried with other types of asyncs as well - same result) - my only doubt is that maybe they DO - and have a different "async priority" / are queued as a batch.
There is no place in the specs that explicitly says they run async in native, anyway.

What is the definite proof...?

EDIT: I'm learning to read the specs / distinguish between the terms, and it definitely says the handlers are "called" and not queued.

Copy link
Copy Markdown
Member

@Josh-Cena Josh-Cena Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, now I'm not sure what the issue is claiming.

For a "native" event:

  1. The browser receives a mouse click; it enqueues all registered event handlers on the task queue.
  2. At some indefinite time in the future, the event handler is successfully dequeued from the task queue and gets executed. It's asynchronous in the sense that it doesn't reuse the job of any other JavaScript execution—it's the entrypoint of a whole JavaScript job. It's synchronous in the sense that the browser doesn't "await" it, whatever that should mean when there's no relevant return value anyway.

Imagine this:

btn.addEventListener("click", function onclick1() { foo(); });
btn.addEventListener("click", function onclick2() { bar(); });

The job queue:

job 1: whatever code is currently running
job 2: click event handler 1
  stack frame 1: onclick1()
  stack frame 2: foo()
job 3: click event handler 2
  stack frame 1: onclick2()
  stack frame 2: bar()

When job 2 executes, onclick1 may schedule more jobs via setTimeout, new Promise, etc. Because new Promise makes a microtask with higher priority, when job 2 finishes, the next task the browser picks out will be the microtask instead of job 3, which has to wait a bit more. This is the speculation behavior you have observed.

For dispatchEvent:

  1. The function gets called. All registered event handlers get called synchronously.
  2. Each event handler executes in the current JavaScript job. It's definitely synchronous by every definition of synchronicity.

The job queue:

job 1: whatever code is currently running
  stack frame 1: dispatchEvent()
  stack frame 2: onclick1()
  stack frame 3: foo()
  ...
  stack frame 2: onclick2()
  stack frame 3: bar()

Nothing is allowed to execute between onclick1() and onclick2() because it's one synchronous block.

My acceptance of the original issue was that the word "asynchronous" is ambiguous in the context of native events because an event handler cannot observe its asynchronicity anyway, when by definition it has no relevant caller or callee. The key point here is that the event handler executes in a separate JavaScript job.

Now I look back at your suggestion, I don't think it makes a lot of sense either:

Unlike "native" events, which are fired by the browser and invoke dispatchEvent asynchronously via the [event loop]

You are implying the following:

job 1: whatever code is currently running
job 2: all click event handlers
  stack frame 1: dispatchEvent()
  stack frame 2: onclick1()
  stack frame 3: foo()
  ...
  stack frame 2: onclick2()
  stack frame 3: bar()

This would mean no other job is allowed to execute between onclick1() and onclick2(). This is false.

So I think there's something to be rewritten about this paragraph, but not your suggestion verbatim. Sorry I didn't read your suggestion in detail when I triaged the issue.

Copy link
Copy Markdown
Author

@yuval-a yuval-a Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, the mental model (for native events) is more like:
job 1: start internal event dispatch process, which are these steps:
https://dom.spec.whatwg.org/#dispatching-events
these include, for each of the 3 phases: capturing, target, bubbling:
cloning the event handlers registered on each EventTarget object along the "event path" - and calling them one-by-one (like in a for-loop. Specs say: "For each"):
https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke

the key point/question here, again, is if they are called "asynchronously" (as in what JS developers usually expect when they see this term) or not;
They are called in what the specs define as "callback invocation"
https://webidl.spec.whatwg.org/#js-invoking-callback-functions

which, as I'm researching thus far, is not the same as what variations of "running asynchronously" usually mean. It is a specific mechanism, that includes cleanup and microtasks checkpoint after each, but not a "macrotask" checkpoint.
i.e. Promises/Microtasks queued from a listener will run when it's done, but any other "async macrotask" will only run after ALL listeners finished running.

I've seen all sorts of discussions surrounding this, and I also saw this:
whatwg/dom#1308
(A proposal for "asynchronous event listeners" - which I think strengthens my point that event listeners are not considered "asynchronous" in the common/usual way).

so, I think what's actually happening is something that's between the first model you describe in your comment and the second one (that you describe as what I was implying).

And in that model: queued microtasks will run in-between, queued macrotasks will not run in-between, only after.

The issue may be mostly "technical semantics";
I still think the current phrasing:

Unlike "native" events, which are fired by the browser and invoke event handlers asynchronously via the event loop

does not reflect what is technically happening; Event handlers do not go up the event loop the same way other "usual" async tasks usually do.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all native events are fired in their own task (and thus spawn a new stack per callback, which itself will enter the microtask checkpoint when cleaning).
For instance the <iframe>'s load event will fire synchronously at document insertion if its src is about:blank: https://jsfiddle.net/0cus135z/

Copy link
Copy Markdown
Author

@yuval-a yuval-a Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kaiido Interesting.
BUT... your code calls document.body.append(i); - which is conceptually like calling element.click() (it's a different kind of "dispatch" - it spawns from JS - like an "artificial click" - meaning it's not a "native event" "conformally" - it doesn't come from "outside" (e.g. an actual mouse click detected by the OS );

Note that once I put the iframe in the html (without src) - and even set src to about:blank from JS - the expected behavior happens.
See variant of your jsfiddle: https://jsfiddle.net/eqt29b6x/2/

Or it could be that the createElement of the iframe from JS is what's making the difference - this can make sense because in the first variation JS is the source of creation of the iframe, and in the second variation it's the HTML parser - which is in the host/Blink layer - so this also might be the reason for the load behaving differently between variations - it's like a "native load".
Either way I don't think it shows an exception.

Also I saw this:
whatwg/html#4965
But not sure is related or even relevant anymore as my first paragraph above suggests.

I'm going to try to commit the PR with dipikabh phrasing from yesterday - in about two more days (to allow some more time for any other "possible exceptions").

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure everyone will understand this term that way. For me at least, a "native event" is an event fired from the UA itself, and not one generated by the user code that would be dispatched by a direct call to dispatchEvent (or by a wrapper like HTMLElement.click()).

Other such events I know of, that would fall into my definition of "synchronous native events": <form>'s formdata and <input> invalid events when the form is submitted programmatically, AbortSignal's abort event (even when tied by AbortSignal.any()), <dialog> before toggle when triggered from close(), popstate when setting location.hash. But there may be many more in APIs I don't know. Events have to be fired from a task only when "the algorithm that triggers the event could be running on a different thread or process" (https://w3ctag.github.io/design-principles/)

Also, while I believe this might be a bug, in Firefox, beforeinput and input events do fire synchronously, even though they'd fall in your definition.

Copy link
Copy Markdown
Author

@yuval-a yuval-a Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would define "native event" as "anything that does not originate from within the runtime of Javascript".
This also aligns with the quote you mention from the design specs - and also with your examples.

Meaning: "if you do it from JS - it's not a "native event"". I think this definition holds - also aligns with your examples - e.g. "submit with JS" vs. the browser's "native submit" (a button with type="submit" inside a form).

I would also argue that possibly any API implementation parts that ARE in JS actually call dispatchEvent.

"native events" are "system events" --- the obvious examples are things like mouse click - but are also true for something that happens in the "outer host browser level" - "browsery things".

The most important insight and anecdote I've learned from all of this is to remember that Javascript still keeps its original intention of acting as an "interface" to "outer" or "native" capabilities (it started as a "scripting language to interface with the browser" after all) - and those widened incredibly far over the years - but this truth still holds.

The mental model is seeing this single V8 thread as the "inside", and the host browser and beyond as the "outside". If it's triggered "programmatically" with JS === not "native".

Copy link
Copy Markdown
Author

@yuval-a yuval-a Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Come to think of it - there is no "official definition" in MDN of what a "native event" is (at least, I asked chat GPT to check and it says there isn't), while "fired by the user agent" IS sometimes used in specs, so maybe we should use:

Unlike calling dispatchEvent() manually, which triggers event handlers to run synchronously in the same stack frame, "native" events - originating from the browser environment, triggers each event handler to run on a separate stack execution frame; any queued microtasks run after each event handler. However, other asynchronous browser operations, such as rendering, run only after all event handlers have finished running.

…ents

Rephrasing clarification about the difference between native events and manually dispatched events in terms of execution context and timing.
asynchronously via the [event loop](/en-US/docs/Web/JavaScript/Reference/Execution_model),
`dispatchEvent()` invokes event handlers _synchronously_. All applicable event
handlers are called and return before `dispatchEvent()` returns.
Unlike calling `dispatchEvent()` manually, which triggers event handlers to run synchronously in the same stack frame, "native" events (dispatched by the browser) triggers each event handler to run on a separate stack execution frame; any queued microtasks run after each event handler. However, other asynchronous browser operations, such as rendering, run only after all event handlers have finished running.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Josh-Cena @dipikabh Looking at this text ^^^ it seems pretty unhelpful to me - should I as a user of this method need to understand stack frames or microtask? No, no I should not - certainly not for this method where knowing this makes no difference.

This was also discussed in #43973, where we were converging towards something like this:

Suggested change
Unlike calling `dispatchEvent()` manually, which triggers event handlers to run synchronously in the same stack frame, "native" events (dispatched by the browser) triggers each event handler to run on a separate stack execution frame; any queued microtasks run after each event handler. However, other asynchronous browser operations, such as rendering, run only after all event handlers have finished running.
Unlike "native" events, which the browser fires by queuing a task on the [event loop](/en-US/docs/Web/JavaScript/Reference/Execution_model), `dispatchEvent()` invokes all applicable event handlers _synchronously_ before returning.

It is possibly not quite correct, but it captures the main point of the original, which is that these events are synchronous, whereas events generated by the browser are not.

Anyway, my take on this is that something simple here is better or deleting the text entirely, as largely irrelevant to readers.

I'll leave to you, and close the other issue as it was "late to the party".

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... or if you leave it to me, I'll just delete this section altogether :-)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fundamental question is whether there's race—i.e., if it's possible for other jobs to execute in between two event handler executions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following @hamishwillee comment about not being too "low-level-ish" (though I argue about his remark regarding microtasks - modern JS developers are familiar with the concept, especially as queueMicrotask is an official API, MDN also has a guide on it (added a link to it in my suggestion), plus the event loop is mentioned - it's just the same amount of "low-level", if not more) I ) and in the spirit of this paragraph being about "native events VS. user events", I now suggest changing to adding this note (while removing that paragraph)

Suggested change
Unlike calling `dispatchEvent()` manually, which triggers event handlers to run synchronously in the same stack frame, "native" events (dispatched by the browser) triggers each event handler to run on a separate stack execution frame; any queued microtasks run after each event handler. However, other asynchronous browser operations, such as rendering, run only after all event handlers have finished running.
> [!NOTE]
> For events dispatched with this method, the [`isTrusted` property of the Event object]
> (/en-US/docs/Web/API/Event/isTrusted) will be `false`. This is also true for events
> triggered programmatically by other means (for example, by calling `element.click()`).
> By contrast, events dispatched by the browser will have `isTrusted` set to `true`.
>
> Additionally, [microtasks](/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide) scheduled
> from event handlers of an event dispatched programmatically will run only after all
> handlers have finished executing. For browser-dispatched events, microtasks may run
> between individual event listener invocations during the dispatch process.
>
> Other browser work, such as rendering, does not occur until after the entire event dispatch and handling has completed, in both cases.

Copy link
Copy Markdown
Author

@yuval-a yuval-a May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Josh-Cena @dipikabh Looking at this text ^^^ it seems pretty unhelpful to me - should I as a user of this method need to understand stack frames or microtask? No, no I should not - certainly not for this method where knowing this makes no difference.

This was also discussed in #43973, where we were converging towards something like this:

It is possibly not quite correct, but it captures the main point of the original, which is that these events are synchronous, whereas events generated by the browser are not.

Anyway, my take on this is that something simple here is better or deleting the text entirely, as largely irrelevant to readers.

I'll leave to you, and close the other issue as it was "late to the party".

@hamishwillee I disagree about the comment about should they be familiar with what a microtask is. Yes - they should, queueMicrotask is official API. MDN has this guide: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide.
The event loop is mentioned - which is an even "lower-level" concept.

I think the questions regarding about whether this paragraph should exist, and if so - is this the right place, are:

  • Would a reader of the dispatchEvent page will likely be interested in a quick summary of the differences between "custom and native"? I say: yes.
  • What would represent the practical differences? I say: mainly the microtasks difference, the non render between handlers, then I also think the isTrusted.

On the other hand - your last suggestion is short and concise - and does "fix" the technical semantic "mistake" that triggered me to open this PR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fundamental question is whether there's race—i.e., if it's possible for other jobs to execute in between two event handler executions.

@Josh-Cena If this is the fundamental question/problem then IMO should be addressed directly rather than highlighting that some things are sync and others are async. I.e. start with the problem.

@yuval-a As a general principle I think we will have to agree to disagree on microtasks - most programmers most of the time do not need to care about that level of detail. Of course it may be that this is a case where the problem can't be explained without them. Fortunately I'm not the reviewer, so I don't need to assess that.

The paragraph on isTrusted seems useful YOu might consider a few words on why it matters (in addition to the link)

Copy link
Copy Markdown
Author

@yuval-a yuval-a May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hamishwillee I thought about just mentioning Promises but it sounds too partial.
Anyway, I think the subject of "do we go there?" about Microtasks - is like a "standards/specs" question. I'm not a long-running contributor to MDN - I came here because I was triggered by the phrasing of that paragraph - but do MDN have a formal "standards" document? like a "style guide"? If not, it should - I was going per what I know already exists. But anyway I emphasis again that I think it's very legit to use that term;

  1. As I mentioned there's actually an official global method with that term in its name (for me that alone is enough to justify it).
  2. Microtasks is the de facto mechanism of "asynchronicity" in Javascript - before it existed it was just "offload to the host browser" for async. To the best of my knowledge adding it was a design choice spawned from: "We need a way to allow these "clean-up/just-before" tasks inside Javascript. It's used for Promise resolve callbacks, but also for Custom Elements reactions, MutationObserver callbacks and likely other stuff. It's a thing in Javascript, like, a major thing.

Anyway, a compromise would be to mention Promises instead and add "Microtasks" parenthesised. Also, I'm leaning more and more towards a note that explains the two different types of events.

This is my next suggestion:

Suggested change
Unlike calling `dispatchEvent()` manually, which triggers event handlers to run synchronously in the same stack frame, "native" events (dispatched by the browser) triggers each event handler to run on a separate stack execution frame; any queued microtasks run after each event handler. However, other asynchronous browser operations, such as rendering, run only after all event handlers have finished running.
> [!NOTE]
> Events dispatched programmatically using `dispatchEvent()` (as well as by other
> means, such as calling [`click()`](/en-US/docs/Web/API/HTMLElement/click))
> are informally known as "synthetic events", while events dispatched by
> the browser are often referred to as "native events".
>
> Synthetic events (including custom events) have their
> [`isTrusted`](/en-US/docs/Web/API/Event/isTrusted) property set to `false`, while
> browser-generated events have it set to `true`. This allows applications
> and browser features to distinguish between browser-generated events
> and script-dispatched events.
>
> Additionally, [Promise](en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
> callbacks (and other microtasks) scheduled from event handlers of
> synthetic events will run only after all event handlers have completed running. For
> browser-dispatched events, microtasks may run between individual event handler
> invocations during dispatch. Other browser work, such as rendering, occurs only after
> the entire event dispatch has completed in both cases.

Copy link
Copy Markdown
Collaborator

@hamishwillee hamishwillee May 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yuval-a Thanks. I think this is getting htere ^^^. I wouldn't necessarily have (all of) it as a note because it isn't really all aside material. You would want to tidy this bit "scheduled from event handlers of "synthetic events will run only after all event handlers have completed running." because it compares event handlers and event handlers without differentiation of the types you mean.

But I am leaving this to @dipikabh and @Josh-Cena

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Content:WebAPI Web API docs size/s [PR only] 6-50 LoC changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Misleading sentence in dispatchEvent() page

6 participants